Author

Aryan Safi and Kelli Marquardt

Published

February 25, 2026

Inflation

Inflation is typically measured as the percentage change in a price index over time. A price index (CPI or PCEPI) is a weighted average of prices:

\[ P_t = \sum_i w_i \times p_{i,t} \]

Object Definition
\(w_i\) weight for item/service \(i\)
\(p_{i,t}\) the price of item/service \(i\) at \(t\)

Inflation is then:

\[ \pi_t = \frac{P_t - P_{t-k}}{P_{t-k}} \]

and \(k\) is typically 12 indicating the year over year inflation in prices provided monthly.

Decomposition into weights and prices

To decompose inflation into (i) prices and (ii) weights, we have two options:

Laspeyres-style (base period weights)

\[ \Delta P_t^L = \underbrace{\sum_i w_{i,t-k} \times \Delta p_i}_{\text{Price Component}} + \underbrace{\sum_i \Delta w_i \times p_{i, t}}_{\text{Weight Component}} \]

Paasche-style (current period weights)

\[ \Delta P_{t-k,t}^P = \underbrace{\sum_i w_{i,t} \times \Delta p_i}_{\text{Price Component}} + \underbrace{\sum_i \Delta w_i \times p_{i, t-k}}_{\text{Weight Component}} \]

Term Definition
\(\Delta p_i\) \(p_{i,t} - p_{i, t-k}\)
\(\Delta w_i\) \(w_{i,t} - w_{i, t-k}\)

Walkthrough Example

The Data

Suppose we have the following healthcare sector with just two services: Medicare and Private

Type Period 0 Period 1
Medicare Price 10 12
Private Price 5 8
Medicare Weight 0.7 0.6
Private Weight 0.3 0.4

Notice that both prices rose, but the weights shifted toward private insurance.

Laspeyres Decomposition

\[ \underbrace{0.7 * (12-10)}_{\text{Medicare}} + \underbrace{0.3 * (8-5)}_{\text{Private}} = 2.3 \tag{Price Component} \]

\[ \underbrace{12 * (0.6-0.7)}_{\text{Medicare}} + \underbrace{8 * (0.4-0.3)}_{\text{Private}} = -0.4 \tag{Weight Component} \]

\[ 2.3 + -0.4 = \boxed{1.9} \tag{Total Change} \]

We can verify this is true by computing inflation directly. We compute the price index in each period and then calculate the difference.

\[ P_0 = 0.7 * 10 + 0.3*5 = 8.5 \]

\[ P_1 = 0.6 * 12 + 0.4 * 8 = 10.4 \]

\[ \Delta P_{0,1} = 10.4 - 8.5 = \boxed{1.9} \]

Verified!

Paasche Decomposition

\[ \underbrace{0.6 * (12-10)}_{\text{Medicare}} + \underbrace{0.4 * (8-5)}_{\text{Private}} = 2.4 \tag{Price Component} \]

\[ \underbrace{10 * (0.6-0.7)}_{\text{Medicare}} + \underbrace{5 * (0.4-0.3)}_{\text{Private}} = -0.5 \tag{Weight Component} \]

\[ 2.4 + (-0.5) = \boxed{1.9} \tag{Total Change} \]

Same change!

Summary

Laspeyres-style Paasche-style Interpretation
Price Component +2.3 +2.4 If consumers had kept their baskets fixed each period, the index would have risen by this much
Weight Component -0.4 -0.5 Consumers shifted toward Private insurance. This is the “substitution” effect. This partially offset price increases.
Total +1.9 +1.9 Total change in the price index between \(t-k\) and \(t\)

Key Conceptual Differences

Type Laspeyres Paasche
Price effect evaluated at Old weights New weights
Weight effect evaluated at New prices Old prices
Emphasizes What inflation would have been without behavioral change What inflation was experienced given consumption behavior

For our project, Laspeyres is useful if we’re trying to answer: “How much did hospital input cost rise, holding service mix constant?” - isolating pure cost pressures. Paasche might be useful if we’re asking “Given how hospitals changed their mix, what drove observed spending growth?” - capturing the realized experience.

By-Payer Decomposition

Note, we can also take the work we have done to aggregate by payer type. Recall the two expressions we created for the Laspeyres Decomposition as an example.

Note

Recall \[ \underbrace{0.7 * (12-10)}_{\text{Medicare}} + \underbrace{0.3 * (8-5)}_{\text{Private}} = 2.3 \tag{Price Component} \]

\[ \underbrace{12 * (0.6-0.7)}_{\text{Medicare}} + \underbrace{8 * (0.4-0.3)}_{\text{Private}} = -0.4 \tag{Weight Component} \]

\[ \underbrace{0.7 * (12-10)}_{\text{Medicare Price}} + \underbrace{12 * (0.6-0.7)}_{\text{Medicare Weight}} = 0.2 \tag{Medicare Component} \]

\[ \underbrace{0.3 * (8-5)}_{\text{Private Price}} + \underbrace{8 * (0.4-0.3)}_{\text{Private Weight}} = 1.7 \tag{Private Component} \]

And finally, we can verify that these also sum to the total inflation.

\[ 1.7 + 0.2 = \boxed{1.9} \]

Industry-Based PPI

Producer Price Index (Monthly)

Code
# read haver ppi 
ppi = import_haver(series = c("R62211A2@PPIR", 
                              "R62211A4@PPIR",
                              "R62211A6@PPIR"),eop = F) |> 
  rename(ppi_Medicare = r62211a2,
         ppi_Medicaid = r62211a4,
         ppi_Other = r62211a6)


ppi |>
  pivot_longer(cols = !date,
               names_to = 'Series',
               values_to = 'Value') |>
  mutate(Series = str_replace_all(Series, 'ppi_', '')) |>
  mutate(Series = ifelse(Series == 'Other', 'Private/Other', Series)) |>
  ggplot(aes(x = date, color = Series, y = Value)) +
  # geom_point() +
  geom_line(linewidth = 1.05) +
  labs(y = 'Producer Price Index - Industry Based', x = 'Month') +
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) +
  scale_x_date(breaks = seq.Date(min(ppi$date), max(ppi$date), by = "2 years"),
               date_labels = "%Y")

Code
DT::datatable(ppi, caption = 'Producer Price Index (Haver)',
              rownames = F, 
              extensions = 'Buttons',
              options = list(
                dom = 'Bfrtip', 
                buttons = c('copy', 'csv', 'excel')
              ))

Producer Price Index (Annual)

Code
# store ppi yearly by taking average 
ppi_yearly = ppi |> 
  mutate(
    year = year(date)
  ) |> 
  group_by(year) |> 
  summarise(
    ppi_Medicare = mean(ppi_Medicare, na.rm = T),
    ppi_Medicaid = mean(ppi_Medicaid, na.rm = T),
    ppi_Other = mean(ppi_Other, na.rm = T)
  )|>
  ungroup()

ppi_yearly |>
  pivot_longer(
    cols = !year,
    names_to = 'Series',
    values_to = 'Value'
  ) |>
  mutate(Series = str_replace_all(Series, 'ppi_', '')) |>
  mutate(Series = ifelse(Series == 'Other', 'Private/Other', Series)) |>
  ggplot(aes(x = year, color = Series, y = Value)) +
  # geom_point() + 
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Producer Price Index - Industry Based',
    x = 'Year'
  ) +
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) + 
  scale_x_continuous(breaks = seq(min(ppi_yearly$year), max(ppi_yearly$year), by = 2))

Producer Price Index % YoY Changes (Monthly)

Code
ppi |>
  mutate(
    ppi_Medicare = 100 * (ppi_Medicare - lag(ppi_Medicare, 12)) / lag(ppi_Medicare, 12),
    ppi_Medicaid = 100 * (ppi_Medicaid - lag(ppi_Medicaid, 12)) / lag(ppi_Medicaid, 12),
    ppi_Other = 100 * (ppi_Other - lag(ppi_Other, 12)) / lag(ppi_Other, 12)
    ) |> 
  na.omit() |> 
  pivot_longer(cols = !date,
               names_to = 'Series',
               values_to = 'Value') |>
  mutate(Series = str_replace_all(Series, 'ppi_', '')) |>
  mutate(Series = ifelse(Series == 'Other', 'Private/Other', Series)) |>
  ggplot(aes(x = date, color = Series, y = Value)) +
  # geom_point() +
  geom_line(linewidth = 1.05) +
  labs(y = 'Producer Price Index - Industry Based (% YoY)', x = 'Month') +
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) +
  scale_x_date(breaks = seq.Date(min(ppi$date), max(ppi$date), by = "2 years"),
               date_labels = "%Y")

Code
DT::datatable(ppi, caption = 'Producer Price Index (Haver)',
              rownames = F, 
              extensions = 'Buttons',
              options = list(
                dom = 'Bfrtip', 
                buttons = c('copy', 'csv', 'excel')
              ))

Producer Price Index % YoY Change (Annual)

Code
# store ppi yearly by taking average
ppi_yearly |>
  mutate(
    ppi_Medicare = 100 * (ppi_Medicare - lag(ppi_Medicare, 1)) / lag(ppi_Medicare, 1),
    ppi_Medicaid = 100 * (ppi_Medicaid - lag(ppi_Medicaid, 1)) / lag(ppi_Medicaid, 1),
    ppi_Other = 100 * (ppi_Other - lag(ppi_Other, 1)) / lag(ppi_Other, 1)
  ) |>
  pivot_longer(
    cols = !year,
    names_to = 'Series',
    values_to = 'Value'
  ) |>
  mutate(Series = str_replace_all(Series, 'ppi_', '')) |>
  mutate(Series = ifelse(Series == 'Other', 'Private/Other', Series)) |>
  ggplot(aes(x = year, color = Series, y = Value)) +
  # geom_point() + 
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Producer Price Index (% YoY)',
    x = 'Year'
  ) +
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) + 
  scale_x_continuous(breaks = seq(min(ppi_yearly$year), max(ppi_yearly$year), by = 2))

Industry-Based Expenditure Shares

Download Data

We will read the expenditure share data from the link CMS NHEA. Since the PCE Price Index does not publish expenditure shares by payer type, it is necessary to obtain weights from elsewhere. These will be found in the Center for Medicaid and Medicare Services (CMS) National Health Expenditure Accounts (NHEA) tables published online. You can find the relevant table at this link CMS NHEA. After downloading, we will read Table 7 which contains the expenditure shares by payer type. From this table, we will create the following variables:

  • Medicaid = G5:G32
  • Medicare = F5:F32
  • Other = C5:C32 + E5:E32 + H5:H32 + I5:I32 = Out of Pocket Payers + Private Health Insurance + Other Health Insurance + Other Third Party Payers

We create the Other variable to match the “Private and all other patients” designation of the data series with Haver Code R62211A6. We verify that the total column (B5:B32) is the sum total of these 3 categories (up to rounding error). We re-calculate this total value by summing the 3 variables above to ensure that the expenditure shares of each payer-type sum to 1.

Code
# check to see if the directory exists
if (!dir.exists(here('data/cms'))) {
  dir.create(here('data/cms'))
}

# check to see if the file is downloaded
if (!file.exists(here('data/cms/nhe-tables.zip'))) {
  download.file(
    url = 'https://www.cms.gov/files/zip/nhe-tables.zip',
    destfile = here('data/cms/nhe-tables.zip'),
    mode = 'wb'
  )
  unzip(
    zipfile = here("data/cms", "nhe-tables.zip"),
    exdir   = here("data/cms", "nhe-tables")
  )
}

# read medicare and medicaid expenditures
medicare = readxl::read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), 
                        range = 'F4:F32', 
                        ) |> 
  as.matrix() |> as.vector()

# read medicare and medicaid expenditures
medicaid = readxl::read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), 
                        range = 'G4:G32', 
                        ) |> 
  as.matrix() |> as.vector()

# read private 
private = readxl::read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), 
                        range = 'E4:E32', 
                        ) |> 
  as.matrix() |> 
  as.vector()

# read out of pocket 
oop = readxl::read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), 
                        range = 'C4:C32', 
                        ) |> 
  as.matrix() |> 
  as.vector()

# read other health insurance 
other_health_ins = read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), 
                        range = 'H4:H32', 
                        ) |> 
  as.matrix() |> 
  as.vector()

# read other 3rd party payers 
third_party = read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), 
                        range = 'I4:I32', 
                        ) |> 
  as.matrix() |> 
  as.vector()

# combine all of these not-medicare, not-medicaid variables into one "other" category 
other = private + oop + other_health_ins + third_party


# read total expenditures 
total = read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), 
                     range = 'B4:B32') |> 
    as.matrix() |> 
  as.vector()


# store year 
year = read_xlsx(here('data/cms/nhe-tables/Table 07 Hospital Care Expenditures.xlsx'), 
                     range = 'A4:A32')[,1] |> 
  as.matrix() |> 
  as.vector()

# combine into one data frame
exp = data.frame(
  year = year, 
  total = total, 
  Medicare = medicare, 
  Medicaid = medicaid, 
  Other = other
) |> 
  mutate(
    across(where(is.numeric), ~ round(., 1))
  ) |> 
  mutate(
    total = Medicare + Medicaid + Other
  ) |> 
  filter(year >= 2000)

Total Expenditures by Payer-Type (Annual)

Code
# note: the total matches them to rounding error. 

# plot 
exp |> 
  rename(`Private/Other` = Other) |> 
  select(-total) |>
  pivot_longer(
    cols = !year, 
    names_to = 'Series', 
    values_to = 'Value'
  ) |> 
  ggplot(aes(x = year, color = Series, y = Value)) + 
  # geom_point(size = 3) +
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Hospital Expenditures',
    x = 'Year',
    subtitle = 'Billions $'
  ) + 
  scale_color_manual(values = ggpubr::get_palette('jco', 4)) + 
  scale_x_continuous(
    breaks = seq(min(exp$year), 
                 max(exp$year), 
                 by = 2))

Code
DT::datatable(exp, caption = 'Hospital Expenditures (Billions)', rownames = F, 
              extensions = 'Buttons',
              options = list(
                dom = 'Bfrtip', 
                buttons = c('copy', 'csv', 'excel')
              ))
Code
rm(list = setdiff(ls(), c('exp', 'ppi', 'ppi_yearly')))

% YoY Change in Total Expenditures by Payer-Type (Annual)

Code
# plot 
exp |> 
  mutate(
    Medicare = 100*(Medicare - lag(Medicare, 1)) / lag(Medicare, 1), 
    Medicaid = 100 * (Medicaid - lag(Medicaid, 1)) / lag(Medicaid, 1), 
    Other = 100 * (Other - lag(Other, 1)) / lag(Other, 1)
  ) |> 
  rename(`Private/Other` = Other) |> 
  select(-total) |>
  na.omit() |> 
  pivot_longer(
    cols = !year, 
    names_to = 'Series', 
    values_to = 'Value'
  ) |> 
  ggplot(aes(x = year, color = Series, y = Value)) + 
  # geom_point(size = 3) +
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Hospital Expenditures',
    x = 'Year',
    subtitle = '% YoY'
  ) + 
  scale_color_manual(values = ggpubr::get_palette('jco', 4)) + 
  scale_x_continuous(
    breaks = seq(min(exp$year), 
                 max(exp$year), 
                 by = 2))

The expenditure data are annual here. We will change this to monthly data by creating a sequence of monthly dates from January 2000 to December 2024, storing the year of each month, and merging the annual expenditure data using this year variable.

Total Expenditures by Payer-Type (Monthly)

Code
exp_monthly = data.frame(
  date = seq.Date(as.Date('2000-01-01'), as.Date('2024-12-01'), by = 'month')
) |> 
  mutate(
    year = year(date)
  ) |> 
  left_join(exp, by = 'year')

exp_monthly |>
  rename(`Private/Other` = Other) |> 
  select(-total, -year) |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Series', 
    values_to = 'Value'
  ) |> 
  ggplot(aes(x = date, color = Series, y = Value)) + 
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Hospital Expenditures',
    x = 'Month',
    subtitle = 'Billions $'
  ) + 
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) + 
  scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) 

% YoY Change in Total Expenditures by Payer-Type (Monthly)

Code
exp_monthly |>
  select(-total, -year) |> 
  mutate(
    Medicare = 100 * (Medicare - lag(Medicare, 12)) / lag(Medicare, 12), 
    Medicaid = 100 * (Medicaid - lag(Medicaid, 12)) / lag(Medicaid, 12),
    Other = 100 * (Other - lag(Other, 12)) / lag(Other, 12)
  ) |> 
    rename(`Private/Other` = Other) |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Series', 
    values_to = 'Value'
  ) |> 
  na.omit() |> 
  ggplot(aes(x = date, color = Series, y = Value)) + 
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Hospital Expenditures',
    x = 'Month',
    subtitle = '% YoY'
    ) + 
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) + 
  scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) 

Next, we compute the expenditure shares of each category.

Expenditure Shares by Payer-Type (Monthly)

Code
exp_monthly = exp_monthly |> 
  mutate(
    shr_Medicare = Medicare / total, 
    shr_Medicaid = Medicaid/total, 
    shr_Other = Other / total
  )

exp_monthly |> 
  rename(`shr_Private/Other` = shr_Other) |> 
  select(date, contains('shr')) |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Series', 
    values_to = 'Value'
  ) |> 
  mutate(
    Series = str_replace_all(Series, 'shr_', '')
  ) |> 
  ggplot(aes(x = date, color = Series, y = Value)) + 
  # geom_point() + 
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Hospital Expenditure Shares',
    x = 'Month'
  ) + 
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) + 
  scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) 

Expenditure Shares by Payer-Type % YoY (Monthly)

Code
exp_monthly |> 
  mutate(
    shr_Medicare = 100*(shr_Medicare - lag(shr_Medicare, 12))/ lag(shr_Medicare, 12), 
    shr_Medicaid = 100*(shr_Medicaid - lag(shr_Medicaid, 12))/ lag(shr_Medicaid, 12), 
    shr_Other = 100*(shr_Other - lag(shr_Other, 12))/ lag(shr_Other, 12)
  )|> 
  na.omit() |> 
  rename(`shr_Private/Other` = shr_Other) |> 
  select(date, contains('shr')) |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Series', 
    values_to = 'Value'
  ) |> 
  mutate(
    Series = str_replace_all(Series, 'shr_', '')
  ) |> 
  ggplot(aes(x = date, color = Series, y = Value)) + 
  # geom_point() + 
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Hospital Expenditure Shares',
    x = 'Month',
    subtitle = '% YoY'
  ) + 
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) + 
  scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) 

Expenditure Shares by Payer-Type % YoY (Annual)

Code
exp |> 
  mutate(
    shr_Medicare = Medicare / total, 
    shr_Medicaid = Medicaid / total, 
    shr_Other = Other / total,
    shr_Medicare = 100*(shr_Medicare - lag(shr_Medicare, 1))/ lag(shr_Medicare, 1), 
    shr_Medicaid = 100*(shr_Medicaid - lag(shr_Medicaid, 1))/ lag(shr_Medicaid, 1), 
    shr_Other = 100*(shr_Other - lag(shr_Other, 1))/ lag(shr_Other, 1)
  )|> 
  na.omit() |> 
  rename(`shr_Private/Other` = shr_Other) |> 
  select(year, contains('shr')) |> 
  pivot_longer(
    cols = !year, 
    names_to = 'Series', 
    values_to = 'Value'
  ) |> 
  mutate(
    Series = str_replace_all(Series, 'shr_', '')
  ) |>
  ggplot(aes(x = year, color = Series, y = Value)) + 
  # geom_point() + 
  geom_line(linewidth = 1.05) + 
  labs(
    y = 'Hospital Expenditure Shares',
    x = 'Year',
    subtitle = '% YoY'
  ) +
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) + 
  scale_x_continuous(
    breaks = seq(min(exp$year), max(exp$year), by = 2)
  ) 

Overall Hospital Inflation

Monthly

Code
# convert ppi to long format 
ppi_long = ppi |> 
  pivot_longer(
    cols = !date, 
    names_to = 'payerType', 
    values_to = 'ppi'
  ) |> 
  mutate(
    payerType = str_replace_all(payerType, 'ppi_', '')
  )

# convert exp to long format 
exp_monthly_long = exp_monthly |> 
  select(date, contains('shr')) |> 
  pivot_longer(
    cols = !date, 
    names_to = 'payerType', 
    values_to = 'wt'
  ) |> 
  mutate(
    payerType = str_replace_all(payerType, 'shr_', '')
  )

# combine the data 
inflation = inner_join(exp_monthly_long, ppi_long, by = c('date', 'payerType')) |> 
  # compute overall inflation
  group_by(date) |> 
  summarise(
    P_t = sum(wt*ppi)
  ) |> 
  ungroup() |> 
  mutate(
    pi_t = 100*(P_t - lag(P_t, 12)) / lag(P_t, 12)
  )

fwrite(inflation, here('data/analysis/industry_based_inflation.csv'))

# read in overall healthcare services inflation
pce = import_haver(
  series = c('jcxfebm@USECON', 'jcsdm@usna'), eop = F
) |> 
  rename(
    `Core PCE` = jcxfebm, 
    `Healthcare` = jcsdm
  ) |> 
  mutate(
    `Core PCE` = 100*(`Core PCE` - lag(`Core PCE`, 12)) / lag(`Core PCE`, 12),
    `Healthcare` = 100*(Healthcare - lag(Healthcare, 12)) / lag(Healthcare, 12)
  )
Code
# plot overall 
inflation |> 
  left_join(pce, by = 'date')  |> 
  rename(Hospital = pi_t) |> 
  select(date, Hospital, `Core PCE`, Healthcare) |> 
  na.omit() |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Type', 
    values_to = 'Inflation'
  ) |> 
  ggplot(aes(x = date, y = Inflation, color = Type)) +
  geom_line(linewidth = 1.05) +
  # geom_hline(yintercept = mean(inflation$pi_t, na.rm = T), linetype = 'dashed') + 
  labs(
    x = 'Month',
    y = 'Inflation',
    subtitle = '% YoY'
  ) + 
    scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) + 
    scale_color_manual(values = ggpubr::get_palette('jco', 3)) 

Full Sample
Code
# plot overall 
inflation |> 
  left_join(pce, by = 'date')  |> 
  rename(Hospital = pi_t) |> 
  select(date, Hospital, `Core PCE`, Healthcare) |> 
  na.omit() |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Type', 
    values_to = 'Inflation'
  ) |> 
  filter(date < '2015-01-01') |> 
  ggplot(aes(x = date, y = Inflation, color = Type)) +
  geom_line(linewidth = 1.05) +
  # geom_hline(yintercept = mean(inflation$pi_t, na.rm = T), linetype = 'dashed') + 
  labs(
    x = 'Month',
    y = 'Inflation',
    subtitle = '% YoY'
  ) + 
    scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) + 
    scale_color_manual(values = ggpubr::get_palette('jco', 3)) 

Pre-2015 Sample
Code
# plot overall 
inflation |> 
  left_join(pce, by = 'date')  |> 
  rename(Hospital = pi_t) |> 
  select(date, Hospital, `Core PCE`, Healthcare) |> 
  na.omit() |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Type', 
    values_to = 'Inflation'
  ) |> 
  filter(date >= '2015-01-01') |> 
  ggplot(aes(x = date, y = Inflation, color = Type)) +
  geom_line(linewidth = 1.05) +
  # geom_hline(yintercept = mean(inflation$pi_t, na.rm = T), linetype = 'dashed') + 
  labs(
    x = 'Month',
    y = 'Inflation',
    subtitle = '% YoY'
  ) + 
    scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) + 
    scale_color_manual(values = ggpubr::get_palette('jco', 3)) 

Post-2015 Sample
Code
legend = c('Core PCE' = '#0073C2FF', 
           'Healthcare' = '#EFC000FF', 
           'Hospital' = '#868686ff')
# healthcare vs. core pce 
inflation |> 
  left_join(pce, by = 'date')  |> 
  rename(Hospital = pi_t) |> 
  select(date, Healthcare, Hospital) |> 
  na.omit() |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Type', 
    values_to = 'Inflation'
  ) |> 
  ggplot(aes(x = date, y = Inflation, color = Type)) +
  geom_line(linewidth = 1.05) +
  # geom_hline(yintercept = mean(inflation$pi_t, na.rm = T), linetype = 'dashed') + 
  labs(
    x = 'Month',
    y = 'Inflation',
    subtitle = '% YoY'
  ) + 
    scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) + 
    scale_color_manual(values = legend) 

Full Sample Partitioned Pairwise
Code
# healthcare vs. hospital 
inflation |> 
  left_join(pce, by = 'date')  |> 
  rename(Hospital = pi_t) |> 
  select(date, `Core PCE`, Healthcare) |> 
  na.omit() |> 
  pivot_longer(
    cols = !date, 
    names_to = 'Type', 
    values_to = 'Inflation'
  ) |> 
  ggplot(aes(x = date, y = Inflation, color = Type)) +
  geom_line(linewidth = 1.05) +
  # geom_hline(yintercept = mean(inflation$pi_t, na.rm = T), linetype = 'dashed') + 
  labs(
    x = 'Month',
    y = 'Inflation',
    subtitle = '% YoY'
  ) + 
    scale_x_date(
    breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
    date_labels = "%Y"
  ) + 
    scale_color_manual(values = legend) 

Full Sample Partitioned Pairwise

Annual

We repeat the same exercise at the annual frequency. We compute expenditure shares from the annual CMS data directly (rather than spreading them across months) and take the annual average of the monthly PPI series. The weighted price index \(P_t = \sum_i w_{i,t} \times p_{i,t}\) is then computed year-by-year, and year-over-year inflation is \(\pi_t = 100 \times (P_t - P_{t-1}) / P_{t-1}\).

Code
# store annual shares and pivot longer
exp_year_long = exp |> 
  mutate(shr_Medicare = Medicare / total, 
         shr_Medicaid = Medicaid / total, 
         shr_Other = Other / total) |>
  select(year, contains('shr'))  |> 
  pivot_longer(
    cols = !year, 
    names_to = 'payerType', 
    values_to = 'wt'
  ) |> 
  mutate(
    payerType = str_replace_all(payerType, 'shr_', '')
  ) |> 
  mutate(
    year = as.integer(year)
  )

# store annual ppi long 
ppi_yearly_long = ppi_yearly |> 
  pivot_longer(
    cols = !year, 
    names_to = 'payerType', 
    values_to = 'ppi'
  ) |> 
  mutate(
    payerType = str_replace_all(payerType, 'ppi_', '')
  )

# combine the data 
inflation_annual = inner_join(exp_year_long, ppi_yearly_long, by = c('year', 'payerType')) |> 
  # compute overall inflation
  group_by(year) |> 
  summarise(
    P_t = sum(wt*ppi)
  ) |> 
  ungroup() |> 
  mutate(
    pi_t = 100*(P_t - lag(P_t, 1)) / lag(P_t, 1)
  )

fwrite(inflation_annual, here('data/analysis/industry_based_inflation_annual.csv'))



inflation_annual |>
  # left_join(pce, by = 'date')  |>
  rename(Hospital = pi_t) |>
  # select(date, Hospital, `Core PCE`, Healthcare) |>
  select(year, Hospital) |>
  na.omit() |>
  # pivot_longer(cols = !year,
  #              names_to = 'Type',
  #              values_to = 'Inflation') |>
  # ggplot(aes(x = date, y = Inflation, color = Type)) +
  ggplot(aes(x = year, y = Hospital)) +
  geom_line(linewidth = 1.05) +
  # geom_hline(yintercept = mean(inflation$pi_t, na.rm = T), linetype = 'dashed') +
  labs(x = 'Year', y = 'Inflation', 
       subtitle = '% YoY') +
  scale_x_continuous(breaks = seq(min(exp_year_long$year), max(exp_year_long$year), by = 2))

Code
# scale_color_manual(values = ggpubr::get_palette('jco', 3))

Industry Based Decomposition of Hospital Inflation (Monthly)

Code
legend = ggpubr::get_palette('jco', 2)
names(legend) = c('Price Component', 'Weight Component')
inner_join(exp_monthly_long, ppi_long, by = c('date', 'payerType')) |>
  # get price component of the laspeyres-style decomp
  group_by(payerType) |>
  mutate(
    price_component_L = lag(wt, 12) * (ppi - lag(ppi, 12)),
    weight_component_L = ppi * (wt - lag(wt, 12))
  ) |>
  ungroup() |>
  group_by(date) |>
  summarise(
    `Price Component` = sum(price_component_L, na.rm = T),
    `Weight Component` = sum(weight_component_L, na.rm = T)
  ) |>
  ungroup() |>
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation, by = 'date') |>
  mutate(
    `Price Component`  = `Price Component`  / lag(P_t, 12) * 100,
    `Weight Component` = `Weight Component` / lag(P_t, 12) * 100,
    pi_t = pi_t
  ) |>
  pivot_longer(
    cols = c(`Price Component`, `Weight Component`),
    names_to = 'Component',
    values_to = 'Contribution'
  ) |>
  # check the inflation total matches pi_t
  group_by(date) |>
  mutate(check = sum(Contribution)) |>
  ungroup() |>
  mutate(flag = ifelse(round(check - pi_t, 5) == 0, F, T)) |>
  filter(date >= '2001-01-01') |>
  # summary()
  ggplot(aes(x = date)) +
  geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') +
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) +
  scale_x_date(breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
               date_labels = '%Y') +
  labs(x = 'Month', y = 'Percentage Point')

Code
legend = ggpubr::get_palette('jco', 3)
names(legend) = c('Private/Other', 'Medicare', 'Medicaid')
inner_join(exp_monthly_long, ppi_long, by = c('date', 'payerType')) |>
  mutate(
    payerType = ifelse(payerType == 'Other', 'Private/Other', payerType)
  ) |> 
  # get price component of the laspeyres-style decomp
  group_by(payerType) |>
  mutate(
    price_component_L = lag(wt, 12) * (ppi - lag(ppi, 12)),
    weight_component_L = ppi * (wt - lag(wt, 12))
  ) |>
  ungroup() |> 
  # store the contribution from each 
  mutate(
    contribution_L = price_component_L + weight_component_L
  ) |> 
  select(
    date, payerType, contribution_L
  ) |> 
    arrange(payerType, date) |> 
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation, by = 'date') |> 
  group_by(payerType) |> 
  mutate(
    Contribution = contribution_L / lag(P_t, 12) * 100,
    pi_t = pi_t
  ) |>
  ungroup() |> 
  # check the inflation total matches pi_t
  group_by(date) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |> 
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  filter(date >= '2001-01-01') |>
  ggplot(aes(x = date)) +
  geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
  scale_x_date(breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
               date_labels = '%Y') +
  labs(
    x = 'Month', 
    y = 'Percentage Points',
    fill = 'Payer Type', 
    color = 'Payer Type'
  )

Code
legend = ggpubr::get_palette('jco', 2)
names(legend) = c('Price Component', 'Weight Component')
inner_join(exp_monthly_long, ppi_long, by = c('date', 'payerType')) |>
  # get price component of the paasche-style decomp
  group_by(payerType) |>
  mutate(
    price_component_P = wt * (ppi - lag(ppi, 12)),
    weight_component_P = lag(ppi, 12) * (wt - lag(wt, 12))
  ) |>
  ungroup() |>
  group_by(date) |>
  summarise(
    `Price Component` = sum(price_component_P, na.rm = T),
    `Weight Component` = sum(weight_component_P, na.rm = T)
  ) |>
  ungroup() |>
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation, by = 'date') |>
  mutate(
    `Price Component`  = `Price Component`  / lag(P_t, 12) * 100,
    `Weight Component` = `Weight Component` / lag(P_t, 12) * 100,
    pi_t = pi_t 
  ) |> 
  pivot_longer(
    cols = c(`Price Component`, `Weight Component`),
    names_to = 'Component',
    values_to = 'Contribution'
  ) |>
  # check the inflation total matches pi_t
  group_by(date) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |>
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  filter(date >= '2001-01-01') |> 
  # summary()
  ggplot(aes(x = date)) +
  geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
    scale_x_date(breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
               date_labels = '%Y') +
  labs(
    x = 'Month', 
    y = 'Percentage Point'
  )

Code
legend = ggpubr::get_palette('jco', 3)
names(legend) = c('Private/Other', 'Medicare', 'Medicaid')
inner_join(exp_monthly_long, ppi_long, by = c('date', 'payerType')) |>
  mutate(
    payerType = ifelse(payerType == 'Other', 'Private/Other', payerType)
  ) |> 
  # get price component of the paasche-style decomp
  group_by(payerType) |>
  mutate(
    price_component_P = wt * (ppi - lag(ppi, 12)),
    weight_component_P = lag(ppi, 12) * (wt - lag(wt, 12))
  ) |>
  ungroup() |> 
  # store the contribution from each 
  mutate(
    contribution_P = price_component_P + weight_component_P
  ) |> 
  select(
    date, payerType, contribution_P
  ) |> 
    arrange(payerType, date) |> 
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation, by = 'date') |> 
  group_by(payerType) |> 
  mutate(
    Contribution = contribution_P / lag(P_t, 12) * 100,
    pi_t = pi_t 
  ) |>
  ungroup() |> 
  # check the inflation total matches pi_t
  group_by(date) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |> 
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  filter(date >= '2001-01-01') |>
  ggplot(aes(x = date)) +
  geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
    scale_x_date(breaks = seq(min(exp_monthly$date), max(exp_monthly$date), by = "2 years"),
               date_labels = '%Y') +
  labs(
    x = 'Month', 
    y = 'Percentage Points',
    fill = 'Payer Type', 
    color = 'Payer Type'
  )

Industry Based Decomposition of Hospital Inflation (Annual)

We now decompose annual hospital inflation into price and weight components using the same Laspeyres and Paasche identities applied to the annual data. The lag is 1 year rather than 12 months. Each component is normalized by \(P_{t-1}\) and multiplied by 100 so the contributions are in percentage points and sum to \(\pi_t\).

The Laspeyres decomposition evaluates price changes at prior-year weights and weight changes at current-year prices: \(\Delta P_t^L = \sum_i w_{i,t-1} \Delta p_i + \sum_i \Delta w_i \, p_{i,t}\).

Code
legend = ggpubr::get_palette('jco', 2)
names(legend) = c('Price Component', 'Weight Component')
inner_join(exp_year_long, ppi_yearly_long, by = c('year', 'payerType')) |>
  # get price component of the laspeyres-style decomp
  group_by(payerType) |>
  mutate(
    price_component_L = lag(wt, 1) * (ppi - lag(ppi, 1)),
    weight_component_L = ppi * (wt - lag(wt, 1))
  ) |>
  ungroup() |>
  group_by(year) |>
  summarise(
    `Price Component` = sum(price_component_L, na.rm = T),
    `Weight Component` = sum(weight_component_L, na.rm = T)
  ) |>
  ungroup() |>
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation_annual, by = 'year') |>
  mutate(
    `Price Component`  = `Price Component`  / lag(P_t, 1) * 100,
    `Weight Component` = `Weight Component` / lag(P_t, 1) * 100,
    pi_t = pi_t
  ) |> 
  pivot_longer(
    cols = c(`Price Component`, `Weight Component`),
    names_to = 'Component',
    values_to = 'Contribution'
  ) |>
  # check the inflation total matches pi_t
  group_by(year) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |>
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |>
  filter(year >= 2001) |>
  # summary()
  ggplot(aes(x = year)) +
  geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
  scale_x_continuous(
    breaks = seq(min(exp_year_long$year), max(exp_year_long$year), by = 2) 
  ) + 
  labs(
    x = 'Year', 
    y = 'Percentage Point'
  )

Rather than splitting into price vs. weight, we aggregate each payer’s total contribution (price + weight) to show how much of overall inflation is attributable to Medicare, Medicaid, and Private/Other.

Code
legend = ggpubr::get_palette('jco', 3)
names(legend) = c('Private/Other', 'Medicare', 'Medicaid')
inner_join(exp_year_long, ppi_yearly_long, by = c('year', 'payerType')) |>
  mutate(payerType = ifelse(payerType == 'Other', 'Private/Other', payerType)) |>
  # get price component of the laspeyres-style decomp
  group_by(payerType) |>
  mutate(
    price_component_L = lag(wt, 1) * (ppi - lag(ppi, 1)),
    weight_component_L = ppi * (wt - lag(wt, 1))
  ) |>
  ungroup() |>
  # store the contribution from each
  mutate(
    contribution_L = price_component_L + weight_component_L
  ) |>
  select(
    year, payerType, contribution_L
  ) |>
    arrange(payerType, year) |>
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation_annual, by = 'year') |>
  group_by(payerType) |>
  mutate(
    Contribution = contribution_L / lag(P_t, 1) * 100,
    pi_t = pi_t
  ) |>
  ungroup() |>
  # check the inflation total matches pi_t
  group_by(year) |>
  mutate(check = sum(Contribution)) |>
  ungroup() |>
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |>
  filter(year >= 2001) |>
  ggplot(aes(x = year)) +
  geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
  scale_x_continuous(
    breaks = seq(min(exp_year_long$year), max(exp_year_long$year), by = 2) 
  ) +
  labs(
    x = 'Year', 
    y = 'Percentage Points',
    fill = 'Payer Type', 
    color = 'Payer Type'
  )

The Paasche decomposition evaluates price changes at current-year weights and weight changes at prior-year prices: \(\Delta P_t^P = \sum_i w_{i,t} \Delta p_i + \sum_i \Delta w_i \, p_{i,t-1}\).

Code
legend = ggpubr::get_palette('jco', 2)
names(legend) = c('Price Component', 'Weight Component')
inner_join(exp_year_long, ppi_yearly_long, by = c('year', 'payerType')) |>
  # get price component of the paasche-style decomp
  group_by(payerType) |>
  mutate(
    price_component_P = wt * (ppi - lag(ppi, 1)),
    weight_component_P = lag(ppi, 1) * (wt - lag(wt, 1))
  ) |>
  ungroup() |>
  group_by(year) |>
  summarise(
    `Price Component` = sum(price_component_P, na.rm = T),
    `Weight Component` = sum(weight_component_P, na.rm = T)
  ) |>
  ungroup() |>
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation_annual, by = 'year') |>
  mutate(
    `Price Component`  = `Price Component`  / lag(P_t, 1) * 100,
    `Weight Component` = `Weight Component` / lag(P_t, 1) * 100,
    pi_t = pi_t
  ) |> 
  pivot_longer(
    cols = c(`Price Component`, `Weight Component`),
    names_to = 'Component',
    values_to = 'Contribution'
  ) |>
  # check the inflation total matches pi_t
  group_by(year) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |>
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  filter(year >= 2001) |> 
  # summary()
  ggplot(aes(x = year)) +
  geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
  scale_x_continuous(
    breaks = seq(min(exp_year_long$year), max(exp_year_long$year), by = 2) 
  ) +
  labs(
    x = 'Year', 
    y = 'Percentage Point'
  )

Same payer-level aggregation as the Laspeyres tab, but using the Paasche identity so that price effects are evaluated at current weights and weight effects at prior-year prices.

Code
legend = ggpubr::get_palette('jco', 3)
names(legend) = c('Private/Other', 'Medicare', 'Medicaid')
inner_join(exp_year_long, ppi_yearly_long, by = c('year', 'payerType')) |>
  mutate(payerType = ifelse(payerType == 'Other', 'Private/Other', payerType)) |>
  # get price component of the paasche-style decomp
  group_by(payerType) |>
  mutate(
    price_component_P = wt * (ppi - lag(ppi, 1)),
    weight_component_P = lag(ppi, 1) * (wt - lag(wt, 1))
  ) |>
  ungroup() |>
  # store the contribution from each
  mutate(
    contribution_P = price_component_P + weight_component_P
  ) |>
  select(
    year, payerType, contribution_P
  ) |> 
    arrange(payerType, year) |> 
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation_annual, by = 'year') |> 
  group_by(payerType) |> 
  mutate(
    Contribution = contribution_P / lag(P_t, 1) * 100,
    pi_t = pi_t
  ) |>
  ungroup() |> 
  # check the inflation total matches pi_t
  group_by(year) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |> 
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  filter(year >= 2001) |>
  ggplot(aes(x = year)) +
  geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') +
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) +
  scale_x_continuous(breaks = seq(min(exp_year_long$year), max(exp_year_long$year), by = 2)) +
  labs(x = 'Year',
       y = 'Percentage Points',
       fill = 'Payer Type',
       color = 'Payer Type')

Commodity-Based Hospital Inflation

Producer Price Index (PPI)

In the commodity-based categorization of the PPI, there are categories for “Hospital outpatient care” and “Hospital inpatient care”, each of which have a subcategory for “General medical and surgical hospitals”. This subcategory for general hospitals has price indexes by payer type, just as detailed for the previous method. The overall categorization looks like:

  1. Hospital outpatient care
    a. General medical and surgical hospitals
    1. Medicare patients
    2. Medicaid patients
    3. Private insurance and all other patients
  2. Hospital inpatient care
    a. General medical and surgical hospitals
    1. Medicare patients
    2. Medicaid patients
    3. Private insurance and all other patients

We need to aggregate the indexes of the same payer type to calculate the overall hospital services price index by that payer.

Code
rm(list=ls())
# read haver ppi 
ppi = import_haver(series = c("P512101B@PPI", 
                              "P511104B@PPI",
                              "P512101C@PPI", 
                              "P511104C@PPI",
                              "P512101D@PPI",
                              "P511104D@PPI"
                              ),
                   eop = F) |>
  rename(ppi_Medicare_in = p512101b,
         ppi_Medicare_out = p511104b,
         ppi_Medicaid_in = p512101c, 
         ppi_Medicaid_out = p511104c, 
         ppi_Other_in = p512101d, 
         ppi_Other_out = p511104d)

ppi |>
  pivot_longer(cols = !date,
               names_to = 'Series',
               values_to = 'Value') |>
  mutate(
    Setting = substr(Series, str_length(Series) - 2, str_length(Series)),
    Setting = str_replace_all(Setting, '_', ''),
    Setting = ifelse(Setting == 'in', 'In Patient', 'Out Patient'),
    `Payer Type` = str_replace_all(Series, 'ppi_|_in|_out', ''),
    `Payer Type` = ifelse(`Payer Type` == 'Other', 'Private/Other', `Payer Type`)
  ) |>
  ggplot(aes(
    x = date,
    color = `Payer Type`,
    linetype = Setting,
    y = Value
  )) +
  geom_line() +
  labs(y = 'Producer Price Index - Commodity Based', x = 'Month') +
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) +
  scale_x_date(breaks = seq.Date(min(ppi$date), max(ppi$date), by = "2 years"),
               date_labels = "%Y") +
  theme(legend.title = element_text(face = 'bold'))

Code
DT::datatable(ppi, caption = 'Commodity Based Producer Price Index',
              rownames = F, 
              extensions = 'Buttons',
              options = list(
                dom = 'Bfrtip', 
                buttons = c('copy', 'csv', 'excel')
              ))

Producer Price Index % YoY (Monthly)

Code
temp = ppi |> as.data.table()
apply(
  X = temp[ , -1], 
  MARGIN = 2, 
  FUN = function(x) {
    100*(x - lag(x, 12)) / lag(x, 12)
  }
) |> 
  as.data.frame() |> 
  mutate(date = ppi$date) |> 
  select(date, everything()) |> 
  pivot_longer(cols = !date,
               names_to = 'Series',
               values_to = 'Value') |>
  na.omit() |> 
  mutate(
    Setting = substr(Series, str_length(Series) - 2, str_length(Series)),
    Setting = str_replace_all(Setting, '_', ''),
    Setting = ifelse(Setting == 'in', 'In Patient', 'Out Patient'),
    `Payer Type` = str_replace_all(Series, 'ppi_|_in|_out', ''),
    `Payer Type` = ifelse(`Payer Type` == 'Other', 'Private/Other', `Payer Type`)
  ) |>
  ggplot(aes(
    x = date,
    color = `Payer Type`,
    linetype = Setting,
    y = Value
  )) +
  geom_line() +
  labs(y = 'Producer Price Index (% YoY)', x = 'Month') +
  scale_color_manual(values = ggpubr::get_palette('jco', 3)) +
  scale_x_date(breaks = seq.Date(min(ppi$date), max(ppi$date), by = "2 years"),
               date_labels = "%Y") +
  theme(legend.title = element_text(face = 'bold'))

Relative Importance Weights

Code
if (!dir.exists(here('data/bls')))  {
  dir.create(here('data/bls'))
}

if (length(list.files(here('data/bls'))) == 0) {
  # store urls
  urls = c(
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2025.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2024.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2023.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2022.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2021.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2020.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2019.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2018.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2017.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2016.xlsx',
    'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-2015.xlsx'
  )
  
  # web download 
  for (i in seq_along(urls)) {
    download.file(url = urls[i],
                  destfile = here(paste0(
                    'data/bls/', 'relimport_', 
                    str_replace_all(urls[i], 'https://www.bls.gov/ppi/tables/wherever-provided-services-and-construction-relative-importance-', '')
                  )),
                  mode = 'wb')
    
  }
}

# store file names
files = list.files(here('data/bls'), full.names = T)

# store commodity base codes
codes = c(
  '5121010111',
  # medicare inpatient
  '5121010112',
  # medicaid inpatient
  '5121010113',
  # other inpatient
  '5111040111',
  # medicare outpatient
  '5111040112',
  # medicaid outpatient
  '5111040113' # other outpatient
)

weights = list()
for (i in seq_along(files)) {
  # read the first file 
  temp = read_xlsx(files[i], skip = 3) |>
    filter(`Commodity code` %in% codes)
  
  # check that the file has relative importance weights for each code (6)
  check = nrow(temp) == 6
  
  if (!check) stop('missing expenditure shares -> inspect!')
  
  # convert to DT
  data.table::setDT(temp)
  
  # drop previous rel import from 2 years ago 
  temp = temp[, -3]
  
  # store year 
  temp$year = as.integer(str_replace_all(colnames(temp)[3], 'Relative importance December ', ''))
  
  # rename for clarity
  colnames(temp)[3] = 'relativeImportance'
  
  # convert to numeric 
  temp$relativeImportance = as.numeric(temp$relativeImportance)
  
  # normalize the relative imortance by the sum to get share of hospital expenditures 
  temp$relativeImportance = temp$relativeImportance / sum(temp$relativeImportance)

  # reorder cols 
  temp = temp |> select(year, everything())
  
  # store in list 
  weights[[i]] = temp
}

# combine data frames 
weights = rbindlist(weights) |> 
  rename(
    code = `Commodity code`
  )

# create label data frame to merge 
label_df = data.frame(
  code = codes,
  Series = c(
    'Medicare_in', 
    'Medicaid_in', 
    'Other_in', 
    'Medicare_out', 
    'Medicaid_out', 
    'Other_out'
  )
)

# merge in labels 
weights = left_join(weights, label_df, by = 'code') |> 
  select(-Index, -code)


# plot 
weights |> 
  ggplot(aes(x = year, color = Series, y = relativeImportance)) + 
  # geom_point() + 
  geom_line() + 
  labs(
    y = 'Expenditure Importance - Commodity Based',
    x = 'Year'
  ) + 
  scale_color_manual(values = ggpubr::get_palette('jco', 6))

Code
DT::datatable(weights, caption = 'Hospital Care Expenditures (Relative Importance)', rownames = F, 
              extensions = 'Buttons',
              options = list(
                dom = 'Bfrtip', 
                buttons = c('copy', 'csv', 'excel')
              ))

As before, we need to convert the annual relative importance weights to the monthly level.

Code
ri_monthly = data.frame(
  date = seq.Date(as.Date('2014-01-01'), as.Date('2024-12-01'), by = 'month')
) |> 
  mutate(
    year = year(date)
  ) |> 
  left_join(weights, by = 'year') |> 
  select(-year)

# plot 
ri_monthly |> 
  ggplot(aes(x = date, color = Series, y = relativeImportance)) + 
  # geom_point() + 
  geom_line() + 
  labs(
    y = 'Expenditure Importance - Commodity Based',
    x = 'Month'
  ) + 
  scale_color_manual(values = ggpubr::get_palette('jco', 6))

Code
DT::datatable(weights, caption = 'Hospital Care Expenditures (Relative Importance)', rownames = F, 
              extensions = 'Buttons',
              options = list(
                dom = 'Bfrtip', 
                buttons = c('copy', 'csv', 'excel')
              ))

Compute Overall Hospital Inflation

Code
# convert ppi to long format 
ppi_long = ppi |> 
  pivot_longer(
    cols = !date, 
    names_to = 'payerType', 
    values_to = 'ppi'
  ) |> 
  mutate(
    payerType = str_replace_all(payerType, 'ppi_', '')
  )

# rename to payer type 
ri_monthly = ri_monthly |> 
  rename(
    payerType = Series
  )

# combine the data 
inflation = inner_join(ri_monthly, ppi_long, by = c('date', 'payerType')) |> 
  # compute overall inflation
  group_by(date) |> 
  summarise(
    P_t = sum(relativeImportance*ppi)
  ) |> 
  ungroup() |> 
  mutate(
    pi_t = 100*(P_t - lag(P_t, 12)) / lag(P_t, 12)
  )

ggplot(inflation, aes(x = date, y = pi_t)) +
  geom_line() +
  geom_hline(yintercept = mean(inflation$pi_t, na.rm = T), linetype = 'dashed') + 
  labs(
    x = 'Month',
    y = 'Hospital Inflation (% YoY)'
  )

Code
fwrite(inflation, here('data/analysis/commodity_based_inflation.csv'))

Comparing Industry-Based and Commodity-Based Inflation

Code
ind_infl = fread(here('data/analysis/industry_based_inflation.csv')) |>
  na.omit() |>
  mutate(date = as.Date(date))

ind_infl = inner_join(ind_infl,
                      inflation,
                      by = 'date',
                      suffix = c('_industry', '_commodity')) |>
  select(date, contains('pi_')) |>
  na.omit() 

ind_infl |> 
  pivot_longer(
    cols = !date, 
    values_to = 'Inflation', 
    names_to = 'Source'
  ) |> 
  mutate(
    Source = str_replace_all(Source, 'pi_t_', '')
  ) |> 
  ggplot(aes(x = date, y = Inflation, color = Source)) + 
  geom_line() + 
  geom_point() +
  scale_color_manual(values = ggpubr::get_palette('jco', 2)) +
  labs(
    y = 'Inflation (% YoY)', 
    x = 'Month', 
    caption = paste0(
      'Correlation: ', 
      round(cor(ind_infl$pi_t_industry, ind_infl$pi_t_commodity),2)
    )
  )

Commodity Based Decomposition of Hospital Inflation

Code
legend = ggpubr::get_palette('jco', 2)
names(legend) = c('Price Component', 'Weight Component')


inner_join(ri_monthly, ppi_long, by = c('date', 'payerType')) |>
  # get price component of the laspeyres-style decomp
  group_by(payerType) |>
  mutate(
    price_component_L = lag(relativeImportance, 12) * (ppi - lag(ppi, 12)),
    weight_component_L = ppi * (relativeImportance - lag(relativeImportance, 12))
  ) |>
  ungroup() |> 
  group_by(date) |>
  summarise(
    `Price Component` = sum(price_component_L, na.rm = T),
    `Weight Component` = sum(weight_component_L, na.rm = T)
  ) |>
  ungroup() |>
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation, by = 'date') |>
  mutate(
    `Price Component`  = `Price Component`  / lag(P_t, 12) * 100,
    `Weight Component` = `Weight Component` / lag(P_t, 12) * 100,
    pi_t = pi_t 
  ) |> 
  pivot_longer(
    cols = c(`Price Component`, `Weight Component`),
    names_to = 'Component',
    values_to = 'Contribution'
  ) |>
  # check the inflation total matches pi_t
  group_by(date) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |>
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  filter(date >= '2015-06-01') |>
  # summary()
  ggplot(aes(x = date)) +
  geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
  labs(
    x = '', 
    y = 'Percentage Point'
  )

Code
legend = ggpubr::get_palette('jco', 3)
names(legend) = c('Other', 'Medicare', 'Medicaid')

inner_join(ri_monthly, ppi_long, by = c('date', 'payerType')) |>
  # get price component of the laspeyres-style decomp
  group_by(payerType) |>
  mutate(
    price_component_L = lag(relativeImportance, 12) * (ppi - lag(ppi, 12)),
    weight_component_L = ppi * (relativeImportance - lag(relativeImportance, 12))
  ) |>
  ungroup() |> 
  # store the contribution from each payer type 
  mutate(
    contribution_L = price_component_L + weight_component_L
  ) |> 
  select(
    date, payerType, contribution_L
  ) |> 
  # collapse the in/out payer types to just one payer type category 
  mutate(payerType = str_replace_all(payerType, '_in', '')) |> 
  mutate(payerType = str_replace_all(payerType, '_out', '')) |> 
  group_by(date, payerType) |> 
  summarise(
    contribution_L = sum(contribution_L, na.rm = T)
  ) |> 
  ungroup() |> 
  arrange(payerType, date) |> 
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation, by = 'date') |> 
  group_by(payerType) |> 
  mutate(
    Contribution = contribution_L / lag(P_t, 12) * 100,
    pi_t = pi_t
  ) |>
  ungroup() |> 
  # check the inflation total matches pi_t
  group_by(date) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |> 
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  # summary()
  filter(date >= '2015-06-01') |>
  ggplot(aes(x = date)) +
  geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
  labs(
    x = '', 
    y = 'Percentage Points',
    fill = 'Payer Type', 
    color = 'Payer Type'
  )

Code
legend = ggpubr::get_palette('jco', 2)
names(legend) = c('Price Component', 'Weight Component')
inner_join(ri_monthly, ppi_long, by = c('date', 'payerType')) |>
  # get price component of the paasche-style decomp
  group_by(payerType) |>
  mutate(
    price_component_P = relativeImportance * (ppi - lag(ppi, 12)),
    weight_component_P = lag(ppi, 12) * (relativeImportance - lag(relativeImportance, 12))
  ) |>
  ungroup() |>
  group_by(date) |>
  summarise(
    `Price Component` = sum(price_component_P, na.rm = T),
    `Weight Component` = sum(weight_component_P, na.rm = T)
  ) |>
  ungroup() |>
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation, by = 'date') |>
  mutate(
    `Price Component`  = `Price Component`  / lag(P_t, 12) * 100,
    `Weight Component` = `Weight Component` / lag(P_t, 12) * 100,
    pi_t = pi_t 
  ) |> 
  pivot_longer(
    cols = c(`Price Component`, `Weight Component`),
    names_to = 'Component',
    values_to = 'Contribution'
  ) |>
  # check the inflation total matches pi_t
  group_by(date) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |>
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  filter(date >= '2015-06-01') |>
  # summary()
  ggplot(aes(x = date)) +
  geom_col(aes(y = Contribution, color = Component, fill = Component), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
  labs(
    x = '', 
    y = 'Percentage Point'
  )

Code
legend = ggpubr::get_palette('jco', 3)
names(legend) = c('Other', 'Medicare', 'Medicaid')
inner_join(ri_monthly, ppi_long, by = c('date', 'payerType')) |>
  # get price component of the paasche-style decomp
  group_by(payerType) |>
  mutate(
    price_component_P = relativeImportance * (ppi - lag(ppi, 12)),
    weight_component_P = lag(ppi, 12) * (relativeImportance - lag(relativeImportance, 12))
  ) |>
  ungroup() |> 
  # store the contribution from each 
  mutate(
    contribution_P = price_component_P + weight_component_P
  ) |> 
  select(
    date, payerType, contribution_P
  ) |> 
  # collapse the in/out payer types to just one payer type category 
  mutate(payerType = str_replace_all(payerType, '_in', '')) |> 
  mutate(payerType = str_replace_all(payerType, '_out', '')) |> 
  group_by(date, payerType) |> 
  summarise(
    contribution_P = sum(contribution_P, na.rm = T)
  ) |> 
  ungroup() |> 
  arrange(payerType, date) |> 
  # normalize by P_{t-k} to convert from level change to percentage change
  left_join(inflation, by = 'date') |> 
  group_by(payerType) |> 
  mutate(
    Contribution = contribution_P / lag(P_t, 12) * 100,
    pi_t = pi_t 
  ) |>
  ungroup() |> 
  # check the inflation total matches pi_t
  group_by(date) |> 
  mutate(check = sum(Contribution)) |> 
  ungroup() |> 
  mutate(
    flag = ifelse(round(check - pi_t, 5) == 0, F, T)
  ) |> 
  filter(date >= '2015-06-01') |>
  ggplot(aes(x = date)) +
  geom_col(aes(y = Contribution, color = payerType, fill = payerType), alpha = .7) +
  geom_point(aes(y = pi_t)) +
  geom_line(aes(y = pi_t), linetype = 'solid') +
  geom_hline(yintercept = 0, linetype = 'solid') + 
  scale_color_manual(values = legend) +
  scale_fill_manual(values = legend) + 
  labs(
    x = '', 
    y = 'Percentage Points',
    fill = 'Payer Type', 
    color = 'Payer Type'
  )